/*
 * Debugging malloc.  SAH, 25-Mar-93.
 *
 * This malloc wrapper adds guards before the beginning and after the
 * end of each heap block.  It checks the entire heap at each call to
 * any of the memory allocation/freeing functions, and reports any
 * blocks which have corrupted guards.
 *
 * It also attempts to spot frees and reallocs of unknown blocks,
 * frees of blocks that have already been freed, etc.
 */

#include "resed.h"


#if DBMALLOC

#undef malloc
#undef free
#undef realloc
#undef checkheap

#define MAGIC 0xDAFFDAFF                /* Identify our blocks */
#define FREED 0xDEADD0D0                /* put size = FREED in freed blocks */
#define SAFESZ (512*1024)               /* If size stored in a block is bigger than this, report a problem */
#define FNAMESZ 20                      /* How much of the source filename to store */


/*
 * The front guard MUST be a whole numbers of words.  Pick guard patterns
 * that are highly unlikely to be written by the program (e.g. if
 * the first byte of backguard were 0, cases of allocating a string
 * one byte too small would go unnoticed.)
 */

static char frontguard[] = {1, 2, 3, 4, 5, 6, 7, 8};
static char backguard[] =  {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
#define FRONTGUARDSZ (sizeof frontguard)
#define BACKGUARDSZ (sizeof backguard)


/*
 * The header to prepend to every heap block.  This is followed
 * immediately by the block itself, which is immediately followed
 * by the backguard bytes (note; the latter might not be word aligned).
 */

typedef struct _dbmallocheader
{
    struct _dbmallocheader *prev, *next;
    unsigned int magic;
    size_t size;
    char filename[FNAMESZ];
    int line;
    char frontguard[FRONTGUARDSZ];
} DBMallocHeaderRec, *DBMallocHeaderPtr;


/*
 * List of known heap blocks
 */

static DBMallocHeaderPtr list = NULL;


/*
 * Return a copy of the filename given, with unreadable
 * characters converted to '?'.  This is used when
 * displaying the filename of potentially corrupt
 * blocks.  Note: the returned string is held in a
 * static buffer.
 */

static char *readable (char *filename)
{
    static char name[FNAMESZ];
    int i;
    sprintf (name, "%.*s", FNAMESZ - 1, filename);
    for (i = 0; i < FNAMESZ - 1; i++)
        if (name[i] != 0 && !isprint(name[i]))
            name[i] = '?';
    return name;
}


/*
 * Walk the list of blocks and report any guard violations.  If 'full'
 * then give a commentary on every block.
 */

static int checkheap (char *caller, char *filename, int line, Bool full)
{
    DBMallocHeaderPtr this;
    int num = 0;
    dprintf ("+");
    for (this = list; this; this = this->next)
    {
        num++;
        if (full)
            dprintf ("\n## (%s@%s:%d): block of %d bytes@%s:%d " _
                     caller _ filename _ line _ this->size _ readable(this->filename) _ this->line);

        if (memcmp((void *) this->frontguard, frontguard, FRONTGUARDSZ))
            dprintf ("\n\007## HEAP INCONSISTENCY (%s@%s:%d): FRONTGUARD: %d bytes@%s:%d " _
                     caller _ filename _ line _ this->size _ readable(this->filename) _ this->line);

        if (this->size > SAFESZ)
            dprintf ("\n\007## HEAP INCONSISTENCY (%s@%s:%d): SAFESIZE VIOLATION: %d bytes@%s:%d " _
                     caller _ filename _ line _ this->size _ readable(this->filename) _ this->line);
        else
        {
            char *trailer = ((char *) this) + sizeof(DBMallocHeaderRec) + this->size;
            if (memcmp((void *) trailer, backguard, BACKGUARDSZ))
                dprintf ("\n\007## HEAP INCONSISTENCY (%s@%s:%d): BACKGUARD: %d bytes@%s:%d " _
                         caller _ filename _ line _ this->size _ readable(this->filename) _ this->line);
        }
    }
    return num;
}


/*
 * Check heap consistency, then perform malloc and link the block into
 * our list.  Used by malloc, calloc and realloc.
 */

static void *_dbmalloc_malloc (char *tag, size_t size, char *filename, int line)
{
    size_t sz;
    DBMallocHeaderPtr header;
    char *mem, *trailer;

    checkheap (tag, filename, line, FALSE);

    sz = sizeof(DBMallocHeaderRec) + size + BACKGUARDSZ;
    if ((mem = (char *) malloc (sz)) == NULL)
    {
        dprintf("\n\007## %s FAILED size=%d@%s:%d " _ tag _ size _ filename _ line);
        return NULL;
    }
    header = (DBMallocHeaderPtr) mem;
    trailer = mem + sizeof(DBMallocHeaderRec) + size;

    header->prev = NULL;                /* push on front of chain */
    header->next = list;
    if (list)
        list->prev = header;
    list = header;
    header->magic = MAGIC;
    header->size = size;                /* requested size */
    sprintf (header->filename, "%.*s", FNAMESZ - 1, filename);
    header->line = line;
    
    memcpy (header->frontguard, frontguard, FRONTGUARDSZ);
    memcpy (trailer, backguard, BACKGUARDSZ);

    return mem + sizeof(DBMallocHeaderRec);
}


/*
 * Called by malloc macro
 */

void *dbmalloc_malloc (size_t size, char *filename, int line)
{
    return _dbmalloc_malloc ("MALLOC", size, filename, line);
}


/*
 * Called by calloc macro
 */

void * dbmalloc_calloc (size_t nmemb, size_t size, char *filename, int line)
{
    void *mem = _dbmalloc_malloc ("CALLOC", nmemb * size, filename, line);
    if (mem)
        memset (mem, 0, nmemb * size);
    return mem;
}


/*
 * Called by free macro.  Check consistency of heap, check for
 * unknown block or already-freed block, free and unlink from list.
 */

void dbmalloc_free (void *ptr, char *filename, int line)
{
    char *mem = ((char *) ptr) - sizeof(DBMallocHeaderRec);
    DBMallocHeaderPtr header = (DBMallocHeaderPtr) mem, this;

    checkheap ("FREE", filename, line, FALSE);
    if (ptr == NULL)
        return;                         /* free of NULL is ignored */

    if (header->size == FREED)
    {
        dprintf ("\n\007## FREE ALREADY FREED _ ptr=%p@%s:%d _ allocated @%s:%d " _
                 (void *) ptr _ filename _ line _ readable (header->filename) _ header->line);
        return;
    }

    /* Seek this block in the list */
    
    for (this = list; this; this = this->next)
    {
        if (this == header)
        {
            if (header->prev)
                header->prev->next = header->next;
            else
                list = header->next;

            if (header->next)
                header->next->prev = header->prev;

            header->size = FREED;
            free ((void *) mem);

            return;
        }
    }

    dprintf ("\n\007## FREE UNKNOWN BLOCK, ptr=%p@%s:%d, allocated @%s:%d " _ 
             (void *) ptr _ filename _ line _
             header->magic == MAGIC ? readable(header->filename) : "<unknown>" _
             header->magic == MAGIC ? header->line : 0);
}


/*
 * Called from realloc macro.
 */

void *dbmalloc_realloc (void *ptr, size_t size, char *filename, int line)
{
    size_t sz;
    char *mem = ((char *) ptr) - sizeof(DBMallocHeaderRec), *newmem, *trailer;
    DBMallocHeaderPtr header = (DBMallocHeaderPtr) mem, this;

    if (ptr == NULL)
        return _dbmalloc_malloc ("REALLOC", size, filename, line);

    checkheap ("REALLOC", filename, line, FALSE);

    /* Seek this block in the list */
    
    for (this = list; this; this = this->next)
        if (this == header)
            break;

    if (this == NULL)
    {
        dprintf ("\n\007## REALLOC UNKNOWN BLOCK, ptr=%p,newsize=%d@%s:%d " _ 
                 ptr _ size _ filename _ line);
        return NULL;
    }
    else if (header->size == FREED)
    {
        dprintf ("\n\007## REALLOC ALREADY FREED, ptr=%p,newsize=%d@%s:%d, allocated @%s:%d " _
                 ptr _ size _ filename _ line _
                 readable (header->filename) _ header->line);
        return NULL;
    }

    /* Calculate new total size */
    sz = sizeof(DBMallocHeaderRec) + size + BACKGUARDSZ;

    block
    {
        DBMallocHeaderRec temp = *header;
        if ((newmem = (char *) realloc ((void *) mem, sz)) == NULL)
        {
            dprintf("\n\007## REALLOC FAILED ptr=%p,newsize=%d@%s:%d, allocated oldsize=%d@%s:%d",
                    ptr _ size _ filename _ line _
                    temp.size _ readable(temp.filename) _ temp.line);
            return NULL;
        }
    }

    /* If the addresses are different, need to fix up the list */

    if (newmem != mem)
    {
        header = (DBMallocHeaderPtr) newmem;
        if (header->prev)
            header->prev->next = header;
        else
            list = header;

        if (header->next)
            header->next->prev = header;
    }

    header->size = size;                /* new size */
    sprintf (header->filename, "%.*s", FNAMESZ - 1, filename);
    header->line = line;
    /* Frontguard preserved */
    trailer = newmem + sizeof(DBMallocHeaderRec) + size;
    memcpy (trailer, backguard, BACKGUARDSZ);

    return newmem + sizeof(DBMallocHeaderRec);
}


/*
 * Report state of heap, number of allocated blocks and report any guard violations
 */

void dbmalloc_checkheap (char *filename, int line, Bool full)
{
    int num;
    Bool pressed;
    swi (OS_Byte,  R0, 129,  R1, KEYCODE_SHIFT ^ 0xFF,  R2, 0xFF,  OUT, R1, &pressed,  END);

    if (pressed)
        debug_file ("Ram:$.Debug");
    dprintf ("\n\nHEAP DIAGNOSTICS:-\n");
    num = checkheap ("CHECK", filename, line, TRUE);
    dprintf ("\n%d HEAP BLOCKS ACTIVE\n" _ num);
    if (pressed)
        debug_file (NULL);
}

#endif /* DBMALLOC */
